import re
from .utils import CfList, CfDict
from .data  import Data
from .functions import equals

# ====================================================================
#
# CellMethods object
#
# ====================================================================

class CellMethods(CfList):
    '''

A CF cell methods object to describe the characteristic of a field
that is represented by cell values.

Each cell method is stored in a dictionary and these dictionaries are
stored in a list-like object. Similarly to a CF cell_methods string,
the order of cell methods in the list is important.

The dictionary representing each cell method recognizes the following
keys (where all keys are optional; the referenced sections are from
the NetCDF Climate and Forecast (CF) Metadata Conventions and words in
double quotes ("...") refer to CF cell_methods components in the
referenced sections):


**Statistics for a combination of axes**

The names of the dimensions involved in a combination of axes are
given as a list (although their order is immaterial), with
corresponding dimension identifiers:

>>> c
<CF CellMethods: latitude: longitude: mean>
>>> list(c)
[{'method'   : 'mean',
  'name'     : ['latitude', 'longitude'],     
  'dim'      : ['dim2, 'dim3']}]

If the string 'area' is used to indicate variation over horizontal
area, then the corresponding dimension is None:

>>> c
<CF CellMethods: area: mean>
>>> list(c)
[{'method'   : 'mean',
  'name'     : ['area'],     
  'dim'      : [None]}]


**Cell methods when there are no coordinates**

These have name strings which are standard names or the string 'area',
with corresponding dimension identifiers of None:

>>> c
<CF CellMethods: time: max>
>>> list(c)
[{'method'   : 'max',
  'name'     : ['time'],     
  'dim'      : [None]}]


>>> c
<CF CellMethods: lat: longitude: mean>
>>> list(c)
[{'method'   : 'mean',
  'name'     : ['lat', 'longitude','],     
  'dim'      : ['dim1', None]}]

>>> c
<CF CellMethods: area: mean>
>>> list(c)
[{'method'   : 'minimum',
  'name'     : ['area'],     
  'dim'      : [None]}]

In the case of 'area', there is no distinction between this cell
method and one for which there appropriate dimensions exist, but the
two cases may be discerned by inspection of the field's space.

'''

    def __init__(self, string=None):
        '''

**Initialization**

:Parameters:

    string : str, optional
        Initialize new instance from a CF-netCDF-like cell methods
        string. See the `parse` method for details. By default an
        empty cell methods is created.

**Examples**

>>> c = CellMethods()
>>> c = CellMethods('time: max: height: mean')

'''
        super(CellMethods, self).__init__()

        self.parse(string)
    #--- End: def

    def __hash__(self):
        '''
x.__hash__() <==> hash(x)

'''
        return hash(self.string)
    #--- End: if

    def __repr__(self):
        '''
x.__repr__() <==> repr(x)

'''
        return '<CF %s: %s>' % (self.__class__.__name__, 
                                ' '.join(self.strings()))
    #--- End: def

    def __str__(self):
        '''
x.__str__() <==> str(x)

'''        
        strings = self.strings()

        if strings:
            string = ['Cell methods    : %s' % strings[0]]
            for s in strings[1:]:
                string.append('                  %s' % s)            
        else:
            return  'Cell methods    : '

        return '\n'.join(string)
    #--- End: def

    @property
    def string(self):
        '''

'''
        return ' '.join(self.strings())
    #--- End: def

    def dump(self, id=None):
         '''

Return a string containing a full description of the instance.

If a cell methods 'name' is followed by a '*' then that cell method is
relevant to the data in a way which may not be precisely defined its
corresponding dimension or dimensions.

:Parameters:

    id : str, optional
       Set the common prefix of component names. By default the
       instance's class name is used.

:Returns:

    out : str
        A string containing the description.

**Examples**

>>> x = c.dump()
>>> print c.dump()
>>> print c.dump(id='cellmethods1')

'''
         if id is None:
             id = self.__class__.__name__
                              
         string = [] #'cell methods\n------------']
         
         for i, s in enumerate(self.strings()):
             string.append('%s[%d] -> %s' % (id, i, s))
             
         return '\n'.join(string)
    #--- End: def

    def equals(self, other, rtol=None, atol=None, traceback=False):
        '''

True if two cell methods are equal, False otherwise.

The `dim` attribute is ignored in the comparison.

:Parameters:

    other : 
        The object to compare for equality.

    atol : float, optional
        The absolute tolerance for all numerical comparisons, By
        default the value returned by the `ATOL` function is used.

    rtol : float, optional
        The relative tolerance for all numerical comparisons, By
        default the value returned by the `RTOL` function is used.

    traceback : bool, optional
        If True then print a traceback highlighting where the two
        instances differ.

:Returns: 

    out : bool
        Whether or not the two instances are equal.

**Examples**

'''
        if self is other:
            return True

        # Check that each instance is the same type
        if self.__class__ != other.__class__:
            if traceback:
                print("%s: Different types: %s, %s" %
                      (self.__class__.__name__,
                       self.__class__.__name__,
                       other.__class__.__name__))
            return False
        #--- End: if

        if len(self) != len(other):
            if traceback:
                print("%s: Different numbers of methods: %d, %d" %
                      (self.__class__.__name__,
                       len(self), len(other)))
            return False
        #--- End: if

        for cm0, cm1 in zip(self, other):
            keys0 = set(cm0)
            keys1 = set(cm1)
         
            if keys0 != keys1: 
                if traceback:
                    print("%s: Different keys: %s" %
                          (self.__class__.__name__,
                           keys0.symmetric_difference(keys1)))
                return False
            #--- End: if

            name0 = cm0['name']
            name1 = cm1['name']
            if len(name0) != len(name1):
                if traceback:
                    print("%s: Different names: %s, %s" %
                          (self.__class__.__name__,
                           repr(name0), repr(name1)))
                return False
            #--- End: if
            if None in name0 or None in name1:
                if traceback:
                    print("%s: Missing name: %s, %s" %
                          (self.__class__.__name__,
                           repr(name0), repr(name1)))
                return False
            #--- End: if

            keys0.remove('dim')
            keys1.remove('dim')
            keys0.remove('name')
            keys1.remove('name')

            if 'interval' in keys0:
                keys0.remove('interval')

                interval0 = cm0['interval'][:]
                interval1 = cm1['interval'][:]

                # Make sure that both lists of intervals are the same
                # length. (If they're not, then the shorter one will
                # have length 1.)
                if len(interval0) < len(name0):
                    interval0 = interval0 * len(name0)
                elif len(interval1) < len(name1):
                    interval1 = interval1 * len(name1)

#                for x, y in zip(interval0, interval1):
#                    if not x.equivalent(y, rtol=rtol, atol=atol):
#                        if traceback:
#                            print("%s: Different interval values: %s, %s" %
#                                  (self.__class__.__name__,
#                                   repr(x), repr(y)))
#                        return False
            #--- End: if

            for key in keys0:
                for x, y in zip(cm0[key], cm1[key]):
                    if not equals(x, y, rtol=rtol, atol=atol,
                                  traceback=traceback):
                        if traceback:
                            print("%s: Different %s values: %s, %s" %
                                  (self.__class__.__name__, key,
                                   repr(x), repr(y)))
                        return False
        #--- End: for

        return True
    #--- End: def

    def equivalent(self, other, rtol=None, atol=None):
        '''

True if two cell methods are equivalent, False otherwise.

The `dim` attribute is ignored in the comparison.

:Parameters:

    other : 
        The object to compare for equality.

    atol : float, optional
        The absolute tolerance for all numerical comparisons, By
        default the value returned by the `ATOL` function is used.

    rtol : float, optional
        The relative tolerance for all numerical comparisons, By
        default the value returned by the `RTOL` function is used.

:Returns: 

    out : bool
        Whether or not the two instances are equivalent.

**Examples**

'''
        if self is other:
            return True

        # Check that each instance is the same type
        if self.__class__ != other.__class__:
            return False

        if len(self) != len(other):
            return False

        for cm0, cm1 in zip(self, other):

            keys0 = set(cm0)
            keys1 = set(cm1)

            if keys0 != keys1: 
                return False

            name0 = cm0['name']
            name1 = cm1['name']
            if len(name0) != len(name1) or (None in name0 or None in name1):
                return False
            
            if 'interval' in keys0:
                keys0.remove('interval')

                interval0 = cm0['interval'][:]
                interval1 = cm1['interval'][:]

                # Make sure that both lists of intervals are the same
                # length. (If they're not, then the shorter one will
                # have length 1.)
                if len(interval0) < len(name0):
                    interval0 = interval0 * len(name0)
                elif len(interval1) < len(name1):
                    interval1 = interval1 * len(name1)

                for x, y in zip(interval0, interval1):
                    if not x.equivalent(y, rtol=rtol, atol=atol, squeeze=True):
                        return False
            #--- End: if

            for key in keys0:
                for x, y in zip(cm0[key], cm1[key]):
                    if not equals(x, y, rtol=rtol, atol=atol,
                                  traceback=False):
                        return False
        #--- End: for

        return True
    #--- End: def

    def has_cellmethods(self, other):
        '''

Return True if and only if this cell methods is a super set of another.

:Parameters:
    other : CellMethods
        The other cell methods for comparison.

:Returns:
    out : bool
        Whether or not this cell methods is a super set of the other.

**Examples**

>>> c = cf.CellMethods('time: mean height: mean area: mean')
>>> d = cf.CellMethods('area: mean time: mean')
>>> c.has_cellmethods(d)
True

'''
        for cm in self:
            match = False
            for i in other:
                for key in ('name', 'method', 'over', 'within'):
                    if key in i:
                        if key not in cm or i[key] != cm[key]:
                            match = False
                            break
                    #--- End: for
                    match = True
                #--- End: for
            #--- End: for
            if match:
                return True
        #--- End: for

        return
    #--- End: def

    def parse(self, string=None):
        '''

Parse a CF cell_methods string into this CellMethods instance in
place.

:Parameters:

    string : str, optional
        The CF cell_methods string to be parsed into the CellMethods
        object. By default the cell methods will be empty.

:Returns:

    None

**Examples**

>>> c = cf.CellMethods()
>>> c = c.parse('time: minimum within years time: mean over years (ENSO years)')    
>>> print c
Cell methods    : time: minimum within years
                  time: mean over years (ENSO years)

'''
        if not string:
            self._list[:] = []
            return

        # Split the cell_methods string into a list of strings ready
        # for parsing into the result list. E.g.
        #   'lat: mean (interval: 1 hour)'
        # maps to 
        #   ['lat:', 'mean', '(', 'interval:', '1', 'hour', ')']
        cell_methods = re.sub('\((?=[^\s])' , '( ', string)
        cell_methods = re.sub('(?<=[^\s])\)', ' )', cell_methods).split()

        while cell_methods:
#            self.region = False

            # Create a new element for this 
            self.append(CfDict())            

            # List of dimensions
            self[-1]['dim'] = []

            # List of names
            self[-1]['name'] = []

            while cell_methods:
                if not cell_methods[0].endswith(':'):
                    break

                self[-1]['name'].append(cell_methods.pop(0)[:-1])            
                self[-1]['dim'].append(None)
            #--- End: while

            if not cell_methods:
                break

            # Method
            self[-1]['method'] = cell_methods.pop(0)

            if not cell_methods:
                break

            # Climatological statistics and statistics which apply to
            # portions of cells
            while cell_methods[0] in ('within', 'where', 'over'):
                term = cell_methods.pop(0)
                self[-1][term] = cell_methods.pop(0)
                if not cell_methods:
                    break
            #--- End: while

            if not cell_methods: 
                break

            # interval and comment
            if cell_methods[0].endswith('('):
                cell_methods.pop(0)

                if not (re.search('^(interval|comment):$', cell_methods[0])):
                    cell_methods.insert(0, 'comment:')
                           
                while not re.search('^\)$', cell_methods[0]):
                    term = cell_methods.pop(0)[:-1]

                    if term == 'interval':
                        if 'interval' not in self[-1]:
                            self[-1]['interval'] = CfList()
                        interval = cell_methods.pop(0)
                        try:
                            interval = float(interval)
                        except ValueError:
                            pass

                        if cell_methods[0] != ')':
                            units = cell_methods.pop(0)
                        else:
                            units = None

                        try:
                            self[-1]['interval'].append(Data(interval, units))
                        except ValueError:
                            del self[-1]['interval']                            
                            print(
"WARNING: Ignoring unknown interval in %s initialization: %s %s" % \
(self.__class__.__name__, interval, units))
                            
                        continue
                    #--- End: if

                    if term == 'comment':
                        comment = CfList()
                        while cell_methods:
                            if cell_methods[0].endswith(')'):
                                break
                            if cell_methods[0].endswith(':'):
                                break
                            comment.append(cell_methods.pop(0))
                        #--- End: while
                        self[-1]['comment'] = ' '.join(comment)
                    #--- End: if

                #--- End: while 

                if cell_methods[0].endswith(')'):
                    cell_methods.pop(0)
            #--- End: if

#            x = [self[-1]['name']]
#            if 'interval' in self[-1]:
#                x.append(self[-1]['interval'])
#
#            x = zip(*sorted(zip(*x)))
##
#            self[-1]['name'] = x[0]
#            if 'interval' in self[-1]:
#                self[-1]['interval'] = x[1]

        #--- End: while

    #--- End: def

    def strings(self):
        '''

Return a list of a CF-netCDF-like string of each cell method.

Note that if the intention is to concatenate the output list into a
string for creating a CF-netCDF cell_methods attribute, then the cell
methods "name" components may need to be modified, where appropriate,
to reflect netCDF variable names.

:Returns:

    out : list
        A string for each cell method.

**Examples**

>>> c = cf.CellMethods('time: minimum within years time: mean over years (ENSO years)')
>>> c.strings()
['time: minimum within years',
 'time: mean over years (ENSO years)']

'''
        strings = []

        for cm in self:

            string = []

#            if sorted(cm['name']) == ['latitude', 'longitude']:
#                string.append('area:')
#            else:
            x = []
            for dim, name in zip(cm['dim'], cm['name']):
                if name is None:
                    if dim is not None:
                        name = dim
                    else:
                        name = '?'

                x.append('%s:' % name)
            #--- End: for
            string.extend(x)
#             #--- End: if

            string.append(cm['method'])

            for portion in ('within', 'where', 'over'):
                if portion in cm:
                    string.extend((portion, cm[portion])) 
            #--- End: for

            if 'interval' in cm:
                x = ['(']

                y = []
                for interval in cm['interval']:
                    print  type(interval), interval.dimensions
                    y.append('interval: %s' % interval.first_datum)
                    if interval.Units:
                        y.append(interval.Units.units)
                #--- End: for
                x.append(' '.join(y))

                if 'comment' in cm:
                    x.append(' comment: %s' % cm['comment'])

                x.append(')')

                string.append(''.join(x))

            elif 'comment' in cm:
                string.append('(%s)' % cm['comment'])
            
            strings.append(' '.join(string))
        #--- End: for

        return strings
    #--- End: def

    def netCDF_translation(self, f):
        '''

Translate netCDF variable names.

:Parameters:

    f : Field
        The field which provides the translation.

:Returns:

    out : CellMethods
        A new cell methods instance with translated names.

**Examples**

>>> c = CellMethods('time: mean lon: mean')
>>> d = c.netCDF_translation(f)

'''
        cell_methods = self.copy()

        space = f.space

        # Change each 'name' value to a standard_name (or space
        # coordinate key) and create the 'dim' key
            
        # From the CF conventions (1.5): In the specification of this
        # attribute, name can be a dimension of the variable, a scalar
        # coordinate variable, a valid standard name, or the word
        # 'area'.
        for cm in cell_methods:

            # Reset dim
            cm['dim'] = []

            if cm['name'] == ['area']:
                cm['dim'].append(None)
                continue
            #--- End: if

            # Still here?
            for i, name in enumerate(cm['name']):
                for dim, ncdim in space.nc_dimensions.iteritems():
                    if name == ncdim:
                        break
                    
                    dim = None
                #--- End: for                    

                if dim is not None:
                    # name is a netCDF dimension name (including
                    # scalar coordinates).
                    cm['dim'].append(dim)
                    if dim in space:
                        cm['name'][i] = space[dim].identity()
                    else:
                        cm['name'][i] = None
                else:                    
                    # name is standard name
                    cm['dim'].append(space.coord({'standard_name': name},
                                                 role='dim', dim=True, one_d=True))
            #--- End: for
        #--- End: for
    
        return cell_methods
    #--- End: def

#--- End: class
